// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE

/*jshint unused:true, eqnull:true, curly:true, bitwise:true */
/*jshint undef:true, latedef:true, trailing:true */
/*global CodeMirror:true */

// erlang mode.
// tokenizer -> token types -> CodeMirror styles
// tokenizer maintains a parse stack
// indenter uses the parse stack

// TODO indenter:
//   bit syntax
//   old guard/bif/conversion clashes (e.g. "float/1")
//   type/spec/opaque

(function(mod) {
  if (typeof exports == 'object' && typeof module == 'object')
    // CommonJS
    mod(require('../../lib/codemirror'));
  else if (typeof define == 'function' && define.amd)
    // AMD
    define(['../../lib/codemirror'], mod);
  // Plain browser env
  else mod(CodeMirror);
})(function(CodeMirror) {
  'use strict';

  CodeMirror.defineMIME('text/x-erlang', 'erlang');

  CodeMirror.defineMode('erlang', function(cmCfg) {
    'use strict';

    /////////////////////////////////////////////////////////////////////////////
    // constants

    var typeWords = ['-type', '-spec', '-export_type', '-opaque'];

    var keywordWords = [
      'after',
      'begin',
      'catch',
      'case',
      'cond',
      'end',
      'fun',
      'if',
      'let',
      'of',
      'query',
      'receive',
      'try',
      'when',
    ];

    var separatorRE = /[\->,;]/;
    var separatorWords = ['->', ';', ','];

    var operatorAtomWords = [
      'and',
      'andalso',
      'band',
      'bnot',
      'bor',
      'bsl',
      'bsr',
      'bxor',
      'div',
      'not',
      'or',
      'orelse',
      'rem',
      'xor',
    ];

    var operatorSymbolRE = /[\+\-\*\/<>=\|:!]/;
    var operatorSymbolWords = [
      '=',
      '+',
      '-',
      '*',
      '/',
      '>',
      '>=',
      '<',
      '=<',
      '=:=',
      '==',
      '=/=',
      '/=',
      '||',
      '<-',
      '!',
    ];

    var openParenRE = /[<\(\[\{]/;
    var openParenWords = ['<<', '(', '[', '{'];

    var closeParenRE = /[>\)\]\}]/;
    var closeParenWords = ['}', ']', ')', '>>'];

    var guardWords = [
      'is_atom',
      'is_binary',
      'is_bitstring',
      'is_boolean',
      'is_float',
      'is_function',
      'is_integer',
      'is_list',
      'is_number',
      'is_pid',
      'is_port',
      'is_record',
      'is_reference',
      'is_tuple',
      'atom',
      'binary',
      'bitstring',
      'boolean',
      'function',
      'integer',
      'list',
      'number',
      'pid',
      'port',
      'record',
      'reference',
      'tuple',
    ];

    var bifWords = [
      'abs',
      'adler32',
      'adler32_combine',
      'alive',
      'apply',
      'atom_to_binary',
      'atom_to_list',
      'binary_to_atom',
      'binary_to_existing_atom',
      'binary_to_list',
      'binary_to_term',
      'bit_size',
      'bitstring_to_list',
      'byte_size',
      'check_process_code',
      'contact_binary',
      'crc32',
      'crc32_combine',
      'date',
      'decode_packet',
      'delete_module',
      'disconnect_node',
      'element',
      'erase',
      'exit',
      'float',
      'float_to_list',
      'garbage_collect',
      'get',
      'get_keys',
      'group_leader',
      'halt',
      'hd',
      'integer_to_list',
      'internal_bif',
      'iolist_size',
      'iolist_to_binary',
      'is_alive',
      'is_atom',
      'is_binary',
      'is_bitstring',
      'is_boolean',
      'is_float',
      'is_function',
      'is_integer',
      'is_list',
      'is_number',
      'is_pid',
      'is_port',
      'is_process_alive',
      'is_record',
      'is_reference',
      'is_tuple',
      'length',
      'link',
      'list_to_atom',
      'list_to_binary',
      'list_to_bitstring',
      'list_to_existing_atom',
      'list_to_float',
      'list_to_integer',
      'list_to_pid',
      'list_to_tuple',
      'load_module',
      'make_ref',
      'module_loaded',
      'monitor_node',
      'node',
      'node_link',
      'node_unlink',
      'nodes',
      'notalive',
      'now',
      'open_port',
      'pid_to_list',
      'port_close',
      'port_command',
      'port_connect',
      'port_control',
      'pre_loaded',
      'process_flag',
      'process_info',
      'processes',
      'purge_module',
      'put',
      'register',
      'registered',
      'round',
      'self',
      'setelement',
      'size',
      'spawn',
      'spawn_link',
      'spawn_monitor',
      'spawn_opt',
      'split_binary',
      'statistics',
      'term_to_binary',
      'time',
      'throw',
      'tl',
      'trunc',
      'tuple_size',
      'tuple_to_list',
      'unlink',
      'unregister',
      'whereis',
    ];

    // upper case: [A-Z] [Ø-Þ] [À-Ö]
    // lower case: [a-z] [ß-ö] [ø-ÿ]
    var anumRE = /[\w@Ø-ÞÀ-Öß-öø-ÿ]/;
    var escapesRE = /[0-7]{1,3}|[bdefnrstv\\"']|\^[a-zA-Z]|x[0-9a-zA-Z]{2}|x{[0-9a-zA-Z]+}/;

    /////////////////////////////////////////////////////////////////////////////
    // tokenizer

    function tokenizer(stream, state) {
      // in multi-line string
      if (state.in_string) {
        state.in_string = !doubleQuote(stream);
        return rval(state, stream, 'string');
      }

      // in multi-line atom
      if (state.in_atom) {
        state.in_atom = !singleQuote(stream);
        return rval(state, stream, 'atom');
      }

      // whitespace
      if (stream.eatSpace()) {
        return rval(state, stream, 'whitespace');
      }

      // attributes and type specs
      if (
        !peekToken(state) &&
        stream.match(/-\s*[a-zß-öø-ÿ][\wØ-ÞÀ-Öß-öø-ÿ]*/)
      ) {
        if (is_member(stream.current(), typeWords)) {
          return rval(state, stream, 'type');
        } else {
          return rval(state, stream, 'attribute');
        }
      }

      var ch = stream.next();

      // comment
      if (ch == '%') {
        stream.skipToEnd();
        return rval(state, stream, 'comment');
      }

      // colon
      if (ch == ':') {
        return rval(state, stream, 'colon');
      }

      // macro
      if (ch == '?') {
        stream.eatSpace();
        stream.eatWhile(anumRE);
        return rval(state, stream, 'macro');
      }

      // record
      if (ch == '#') {
        stream.eatSpace();
        stream.eatWhile(anumRE);
        return rval(state, stream, 'record');
      }

      // dollar escape
      if (ch == '$') {
        if (stream.next() == '\\' && !stream.match(escapesRE)) {
          return rval(state, stream, 'error');
        }
        return rval(state, stream, 'number');
      }

      // dot
      if (ch == '.') {
        return rval(state, stream, 'dot');
      }

      // quoted atom
      if (ch == "'") {
        if (!(state.in_atom = !singleQuote(stream))) {
          if (stream.match(/\s*\/\s*[0-9]/, false)) {
            stream.match(/\s*\/\s*[0-9]/, true);
            return rval(state, stream, 'fun'); // 'f'/0 style fun
          }
          if (stream.match(/\s*\(/, false) || stream.match(/\s*:/, false)) {
            return rval(state, stream, 'function');
          }
        }
        return rval(state, stream, 'atom');
      }

      // string
      if (ch == '"') {
        state.in_string = !doubleQuote(stream);
        return rval(state, stream, 'string');
      }

      // variable
      if (/[A-Z_Ø-ÞÀ-Ö]/.test(ch)) {
        stream.eatWhile(anumRE);
        return rval(state, stream, 'variable');
      }

      // atom/keyword/BIF/function
      if (/[a-z_ß-öø-ÿ]/.test(ch)) {
        stream.eatWhile(anumRE);

        if (stream.match(/\s*\/\s*[0-9]/, false)) {
          stream.match(/\s*\/\s*[0-9]/, true);
          return rval(state, stream, 'fun'); // f/0 style fun
        }

        var w = stream.current();

        if (is_member(w, keywordWords)) {
          return rval(state, stream, 'keyword');
        } else if (is_member(w, operatorAtomWords)) {
          return rval(state, stream, 'operator');
        } else if (stream.match(/\s*\(/, false)) {
          // 'put' and 'erlang:put' are bifs, 'foo:put' is not
          if (
            is_member(w, bifWords) &&
            (peekToken(state).token != ':' ||
              peekToken(state, 2).token == 'erlang')
          ) {
            return rval(state, stream, 'builtin');
          } else if (is_member(w, guardWords)) {
            return rval(state, stream, 'guard');
          } else {
            return rval(state, stream, 'function');
          }
        } else if (lookahead(stream) == ':') {
          if (w == 'erlang') {
            return rval(state, stream, 'builtin');
          } else {
            return rval(state, stream, 'function');
          }
        } else if (is_member(w, ['true', 'false'])) {
          return rval(state, stream, 'boolean');
        } else {
          return rval(state, stream, 'atom');
        }
      }

      // number
      var digitRE = /[0-9]/;
      var radixRE = /[0-9a-zA-Z]/; // 36#zZ style int
      if (digitRE.test(ch)) {
        stream.eatWhile(digitRE);
        if (stream.eat('#')) {
          // 36#aZ  style integer
          if (!stream.eatWhile(radixRE)) {
            stream.backUp(1); //"36#" - syntax error
          }
        } else if (stream.eat('.')) {
          // float
          if (!stream.eatWhile(digitRE)) {
            stream.backUp(1); // "3." - probably end of function
          } else {
            if (stream.eat(/[eE]/)) {
              // float with exponent
              if (stream.eat(/[-+]/)) {
                if (!stream.eatWhile(digitRE)) {
                  stream.backUp(2); // "2e-" - syntax error
                }
              } else {
                if (!stream.eatWhile(digitRE)) {
                  stream.backUp(1); // "2e" - syntax error
                }
              }
            }
          }
        }
        return rval(state, stream, 'number'); // normal integer
      }

      // open parens
      if (nongreedy(stream, openParenRE, openParenWords)) {
        return rval(state, stream, 'open_paren');
      }

      // close parens
      if (nongreedy(stream, closeParenRE, closeParenWords)) {
        return rval(state, stream, 'close_paren');
      }

      // separators
      if (greedy(stream, separatorRE, separatorWords)) {
        return rval(state, stream, 'separator');
      }

      // operators
      if (greedy(stream, operatorSymbolRE, operatorSymbolWords)) {
        return rval(state, stream, 'operator');
      }

      return rval(state, stream, null);
    }

    /////////////////////////////////////////////////////////////////////////////
    // utilities
    function nongreedy(stream, re, words) {
      if (stream.current().length == 1 && re.test(stream.current())) {
        stream.backUp(1);
        while (re.test(stream.peek())) {
          stream.next();
          if (is_member(stream.current(), words)) {
            return true;
          }
        }
        stream.backUp(stream.current().length - 1);
      }
      return false;
    }

    function greedy(stream, re, words) {
      if (stream.current().length == 1 && re.test(stream.current())) {
        while (re.test(stream.peek())) {
          stream.next();
        }
        while (0 < stream.current().length) {
          if (is_member(stream.current(), words)) {
            return true;
          } else {
            stream.backUp(1);
          }
        }
        stream.next();
      }
      return false;
    }

    function doubleQuote(stream) {
      return quote(stream, '"', '\\');
    }

    function singleQuote(stream) {
      return quote(stream, "'", '\\');
    }

    function quote(stream, quoteChar, escapeChar) {
      while (!stream.eol()) {
        var ch = stream.next();
        if (ch == quoteChar) {
          return true;
        } else if (ch == escapeChar) {
          stream.next();
        }
      }
      return false;
    }

    function lookahead(stream) {
      var m = stream.match(/([\n\s]+|%[^\n]*\n)*(.)/, false);
      return m ? m.pop() : '';
    }

    function is_member(element, list) {
      return -1 < list.indexOf(element);
    }

    function rval(state, stream, type) {
      // parse stack
      pushToken(state, realToken(type, stream));

      // map erlang token type to CodeMirror style class
      //     erlang             -> CodeMirror tag
      switch (type) {
        case 'atom':
          return 'atom';
        case 'attribute':
          return 'attribute';
        case 'boolean':
          return 'atom';
        case 'builtin':
          return 'builtin';
        case 'close_paren':
          return null;
        case 'colon':
          return null;
        case 'comment':
          return 'comment';
        case 'dot':
          return null;
        case 'error':
          return 'error';
        case 'fun':
          return 'meta';
        case 'function':
          return 'tag';
        case 'guard':
          return 'property';
        case 'keyword':
          return 'keyword';
        case 'macro':
          return 'variable-2';
        case 'number':
          return 'number';
        case 'open_paren':
          return null;
        case 'operator':
          return 'operator';
        case 'record':
          return 'bracket';
        case 'separator':
          return null;
        case 'string':
          return 'string';
        case 'type':
          return 'def';
        case 'variable':
          return 'variable';
        default:
          return null;
      }
    }

    function aToken(tok, col, ind, typ) {
      return { token: tok, column: col, indent: ind, type: typ };
    }

    function realToken(type, stream) {
      return aToken(
        stream.current(),
        stream.column(),
        stream.indentation(),
        type,
      );
    }

    function fakeToken(type) {
      return aToken(type, 0, 0, type);
    }

    function peekToken(state, depth) {
      var len = state.tokenStack.length;
      var dep = depth ? depth : 1;

      if (len < dep) {
        return false;
      } else {
        return state.tokenStack[len - dep];
      }
    }

    function pushToken(state, token) {
      if (!(token.type == 'comment' || token.type == 'whitespace')) {
        state.tokenStack = maybe_drop_pre(state.tokenStack, token);
        state.tokenStack = maybe_drop_post(state.tokenStack);
      }
    }

    function maybe_drop_pre(s, token) {
      var last = s.length - 1;

      if (0 < last && s[last].type === 'record' && token.type === 'dot') {
        s.pop();
      } else if (0 < last && s[last].type === 'group') {
        s.pop();
        s.push(token);
      } else {
        s.push(token);
      }
      return s;
    }

    function maybe_drop_post(s) {
      if (!s.length) return s;
      var last = s.length - 1;

      if (s[last].type === 'dot') {
        return [];
      }
      if (last > 1 && s[last].type === 'fun' && s[last - 1].token === 'fun') {
        return s.slice(0, last - 1);
      }
      switch (s[last].token) {
        case '}':
          return d(s, { g: ['{'] });
        case ']':
          return d(s, { i: ['['] });
        case ')':
          return d(s, { i: ['('] });
        case '>>':
          return d(s, { i: ['<<'] });
        case 'end':
          return d(s, { i: ['begin', 'case', 'fun', 'if', 'receive', 'try'] });
        case ',':
          return d(s, {
            e: ['begin', 'try', 'when', '->', ',', '(', '[', '{', '<<'],
          });
        case '->':
          return d(s, { r: ['when'], m: ['try', 'if', 'case', 'receive'] });
        case ';':
          return d(s, { E: ['case', 'fun', 'if', 'receive', 'try', 'when'] });
        case 'catch':
          return d(s, { e: ['try'] });
        case 'of':
          return d(s, { e: ['case'] });
        case 'after':
          return d(s, { e: ['receive', 'try'] });
        default:
          return s;
      }
    }

    function d(stack, tt) {
      // stack is a stack of Token objects.
      // tt is an object; {type:tokens}
      // type is a char, tokens is a list of token strings.
      // The function returns (possibly truncated) stack.
      // It will descend the stack, looking for a Token such that Token.token
      //  is a member of tokens. If it does not find that, it will normally (but
      //  see "E" below) return stack. If it does find a match, it will remove
      //  all the Tokens between the top and the matched Token.
      // If type is "m", that is all it does.
      // If type is "i", it will also remove the matched Token and the top Token.
      // If type is "g", like "i", but add a fake "group" token at the top.
      // If type is "r", it will remove the matched Token, but not the top Token.
      // If type is "e", it will keep the matched Token but not the top Token.
      // If type is "E", it behaves as for type "e", except if there is no match,
      //  in which case it will return an empty stack.

      for (var type in tt) {
        var len = stack.length - 1;
        var tokens = tt[type];
        for (var i = len - 1; -1 < i; i--) {
          if (is_member(stack[i].token, tokens)) {
            var ss = stack.slice(0, i);
            switch (type) {
              case 'm':
                return ss.concat(stack[i]).concat(stack[len]);
              case 'r':
                return ss.concat(stack[len]);
              case 'i':
                return ss;
              case 'g':
                return ss.concat(fakeToken('group'));
              case 'E':
                return ss.concat(stack[i]);
              case 'e':
                return ss.concat(stack[i]);
            }
          }
        }
      }
      return type == 'E' ? [] : stack;
    }

    /////////////////////////////////////////////////////////////////////////////
    // indenter

    function indenter(state, textAfter) {
      var t;
      var unit = cmCfg.indentUnit;
      var wordAfter = wordafter(textAfter);
      var currT = peekToken(state, 1);
      var prevT = peekToken(state, 2);

      if (state.in_string || state.in_atom) {
        return CodeMirror.Pass;
      } else if (!prevT) {
        return 0;
      } else if (currT.token == 'when') {
        return currT.column + unit;
      } else if (wordAfter === 'when' && prevT.type === 'function') {
        return prevT.indent + unit;
      } else if (wordAfter === '(' && currT.token === 'fun') {
        return currT.column + 3;
      } else if (wordAfter === 'catch' && (t = getToken(state, ['try']))) {
        return t.column;
      } else if (is_member(wordAfter, ['end', 'after', 'of'])) {
        t = getToken(state, ['begin', 'case', 'fun', 'if', 'receive', 'try']);
        return t ? t.column : CodeMirror.Pass;
      } else if (is_member(wordAfter, closeParenWords)) {
        t = getToken(state, openParenWords);
        return t ? t.column : CodeMirror.Pass;
      } else if (
        is_member(currT.token, [',', '|', '||']) ||
        is_member(wordAfter, [',', '|', '||'])
      ) {
        t = postcommaToken(state);
        return t ? t.column + t.token.length : unit;
      } else if (currT.token == '->') {
        if (is_member(prevT.token, ['receive', 'case', 'if', 'try'])) {
          return prevT.column + unit + unit;
        } else {
          return prevT.column + unit;
        }
      } else if (is_member(currT.token, openParenWords)) {
        return currT.column + currT.token.length;
      } else {
        t = defaultToken(state);
        return truthy(t) ? t.column + unit : 0;
      }
    }

    function wordafter(str) {
      var m = str.match(/,|[a-z]+|\}|\]|\)|>>|\|+|\(/);

      return truthy(m) && m.index === 0 ? m[0] : '';
    }

    function postcommaToken(state) {
      var objs = state.tokenStack.slice(0, -1);
      var i = getTokenIndex(objs, 'type', ['open_paren']);

      return truthy(objs[i]) ? objs[i] : false;
    }

    function defaultToken(state) {
      var objs = state.tokenStack;
      var stop = getTokenIndex(objs, 'type', [
        'open_paren',
        'separator',
        'keyword',
      ]);
      var oper = getTokenIndex(objs, 'type', ['operator']);

      if (truthy(stop) && truthy(oper) && stop < oper) {
        return objs[stop + 1];
      } else if (truthy(stop)) {
        return objs[stop];
      } else {
        return false;
      }
    }

    function getToken(state, tokens) {
      var objs = state.tokenStack;
      var i = getTokenIndex(objs, 'token', tokens);

      return truthy(objs[i]) ? objs[i] : false;
    }

    function getTokenIndex(objs, propname, propvals) {
      for (var i = objs.length - 1; -1 < i; i--) {
        if (is_member(objs[i][propname], propvals)) {
          return i;
        }
      }
      return false;
    }

    function truthy(x) {
      return x !== false && x != null;
    }

    /////////////////////////////////////////////////////////////////////////////
    // this object defines the mode

    return {
      startState: function() {
        return { tokenStack: [], in_string: false, in_atom: false };
      },

      token: function(stream, state) {
        return tokenizer(stream, state);
      },

      indent: function(state, textAfter) {
        return indenter(state, textAfter);
      },

      lineComment: '%',
    };
  });
});
